/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.channel.sctp.oio;

import com.sun.nio.sctp.Association;
import com.sun.nio.sctp.MessageInfo;
import com.sun.nio.sctp.NotificationHandler;
import com.sun.nio.sctp.SctpChannel;

import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelException;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelMetadata;
import io.netty.channel.ChannelOutboundBuffer;
import io.netty.channel.ChannelPromise;
import io.netty.channel.RecvByteBufAllocator;
import io.netty.channel.oio.AbstractOioMessageChannel;
import io.netty.channel.sctp.DefaultSctpChannelConfig;
import io.netty.channel.sctp.SctpChannelConfig;
import io.netty.channel.sctp.SctpMessage;
import io.netty.channel.sctp.SctpNotificationHandler;
import io.netty.channel.sctp.SctpServerChannel;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * {@link io.netty.channel.sctp.SctpChannel} implementation which use blocking
 * mode and allows to read / write {@link SctpMessage}s to the underlying
 * {@link SctpChannel}.
 *
 * Be aware that not all operations systems support SCTP. Please refer to the
 * documentation of your operation system, to understand what you need to do to
 * use it. Also this feature is only supported on Java 7+.
 */
public class OioSctpChannel extends AbstractOioMessageChannel
        implements io.netty.channel.sctp.SctpChannel
{

    private static final InternalLogger logger = InternalLoggerFactory
            .getInstance(OioSctpChannel.class);

    private static final ChannelMetadata METADATA = new ChannelMetadata(false);

    private static final String EXPECTED_TYPE = " (expected: "
            + StringUtil.simpleClassName(SctpMessage.class) + ')';

    private final SctpChannel ch;

    private final SctpChannelConfig config;

    private final Selector readSelector;

    private final Selector writeSelector;

    private final Selector connectSelector;

    private final NotificationHandler<?> notificationHandler;

    private static SctpChannel openChannel()
    {
        try
        {
            return SctpChannel.open();
        }
        catch (IOException e)
        {
            throw new ChannelException("Failed to open a sctp channel.", e);
        }
    }

    /**
     * Create a new instance with an new {@link SctpChannel}.
     */
    public OioSctpChannel()
    {
        this(openChannel());
    }

    /**
     * Create a new instance from the given {@link SctpChannel}.
     *
     * @param ch the {@link SctpChannel} which is used by this instance
     */
    public OioSctpChannel(SctpChannel ch)
    {
        this(null, ch);
    }

    /**
     * Create a new instance from the given {@link SctpChannel}.
     *
     * @param parent the parent {@link Channel} which was used to create this
     *        instance. This can be null if the {@link} has no parent as it was
     *        created by your self.
     * @param ch the {@link SctpChannel} which is used by this instance
     */
    public OioSctpChannel(Channel parent, SctpChannel ch)
    {
        super(parent);
        this.ch = ch;
        boolean success = false;
        try
        {
            ch.configureBlocking(false);
            readSelector = Selector.open();
            writeSelector = Selector.open();
            connectSelector = Selector.open();

            ch.register(readSelector, SelectionKey.OP_READ);
            ch.register(writeSelector, SelectionKey.OP_WRITE);
            ch.register(connectSelector, SelectionKey.OP_CONNECT);

            config = new OioSctpChannelConfig(this, ch);
            notificationHandler = new SctpNotificationHandler(this);
            success = true;
        }
        catch (Exception e)
        {
            throw new ChannelException("failed to initialize a sctp channel",
                    e);
        }
        finally
        {
            if (!success)
            {
                try
                {
                    ch.close();
                }
                catch (IOException e)
                {
                    logger.warn("Failed to close a sctp channel.", e);
                }
            }
        }
    }

    @Override
    public InetSocketAddress localAddress()
    {
        return (InetSocketAddress) super.localAddress();
    }

    @Override
    public InetSocketAddress remoteAddress()
    {
        return (InetSocketAddress) super.remoteAddress();
    }

    @Override
    public SctpServerChannel parent()
    {
        return (SctpServerChannel) super.parent();
    }

    @Override
    public ChannelMetadata metadata()
    {
        return METADATA;
    }

    @Override
    public SctpChannelConfig config()
    {
        return config;
    }

    @Override
    public boolean isOpen()
    {
        return ch.isOpen();
    }

    @Override
    protected int doReadMessages(List<Object> msgs) throws Exception
    {
        if (!readSelector.isOpen())
        {
            return 0;
        }

        int readMessages = 0;

        final int selectedKeys = readSelector.select(SO_TIMEOUT);
        final boolean keysSelected = selectedKeys > 0;

        if (!keysSelected)
        {
            return readMessages;
        }
        // We must clear the selectedKeys because the Selector will never do it.
        // If we do not clear it, the selectionKey
        // will always be returned even if there is no data can be read which
        // causes performance issue. And in some
        // implementation of Selector, the select method may return 0 if the
        // selectionKey which is ready for process has
        // already been in the selectedKeys and cause the keysSelected above to
        // be false even if we actually have
        // something to read.
        readSelector.selectedKeys().clear();
        final RecvByteBufAllocator.Handle allocHandle = unsafe()
                .recvBufAllocHandle();
        ByteBuf buffer = allocHandle.allocate(config().getAllocator());
        boolean free = true;

        try
        {
            ByteBuffer data = buffer.nioBuffer(buffer.writerIndex(),
                    buffer.writableBytes());
            MessageInfo messageInfo = ch.receive(data, null,
                    notificationHandler);
            if (messageInfo == null)
            {
                return readMessages;
            }

            data.flip();
            allocHandle.lastBytesRead(data.remaining());
            msgs.add(new SctpMessage(messageInfo, buffer.writerIndex(
                    buffer.writerIndex() + allocHandle.lastBytesRead())));
            free = false;
            ++readMessages;
        }
        catch (Throwable cause)
        {
            PlatformDependent.throwException(cause);
        }
        finally
        {
            if (free)
            {
                buffer.release();
            }
        }
        return readMessages;
    }

    @Override
    protected void doWrite(ChannelOutboundBuffer in) throws Exception
    {
        if (!writeSelector.isOpen())
        {
            return;
        }
        final int size = in.size();
        final int selectedKeys = writeSelector.select(SO_TIMEOUT);
        if (selectedKeys > 0)
        {
            final Set<SelectionKey> writableKeys = writeSelector.selectedKeys();
            if (writableKeys.isEmpty())
            {
                return;
            }
            Iterator<SelectionKey> writableKeysIt = writableKeys.iterator();
            int written = 0;
            for (;;)
            {
                if (written == size)
                {
                    // all written
                    return;
                }
                writableKeysIt.next();
                writableKeysIt.remove();

                SctpMessage packet = (SctpMessage) in.current();
                if (packet == null)
                {
                    return;
                }

                ByteBuf data = packet.content();
                int dataLen = data.readableBytes();
                ByteBuffer nioData;

                if (data.nioBufferCount() != -1)
                {
                    nioData = data.nioBuffer();
                }
                else
                {
                    nioData = ByteBuffer.allocate(dataLen);
                    data.getBytes(data.readerIndex(), nioData);
                    nioData.flip();
                }

                final MessageInfo mi = MessageInfo.createOutgoing(association(),
                        null, packet.streamIdentifier());
                mi.payloadProtocolID(packet.protocolIdentifier());
                mi.streamNumber(packet.streamIdentifier());
                mi.unordered(packet.isUnordered());

                ch.send(nioData, mi);
                written++;
                in.remove();

                if (!writableKeysIt.hasNext())
                {
                    return;
                }
            }
        }
    }

    @Override
    protected Object filterOutboundMessage(Object msg) throws Exception
    {
        if (msg instanceof SctpMessage)
        {
            return msg;
        }

        throw new UnsupportedOperationException("unsupported message type: "
                + StringUtil.simpleClassName(msg) + EXPECTED_TYPE);
    }

    @Override
    public Association association()
    {
        try
        {
            return ch.association();
        }
        catch (IOException ignored)
        {
            return null;
        }
    }

    @Override
    public boolean isActive()
    {
        return isOpen() && association() != null;
    }

    @Override
    protected SocketAddress localAddress0()
    {
        try
        {
            Iterator<SocketAddress> i = ch.getAllLocalAddresses().iterator();
            if (i.hasNext())
            {
                return i.next();
            }
        }
        catch (IOException e)
        {
            // ignore
        }
        return null;
    }

    @Override
    public Set<InetSocketAddress> allLocalAddresses()
    {
        try
        {
            final Set<SocketAddress> allLocalAddresses = ch
                    .getAllLocalAddresses();
            final Set<InetSocketAddress> addresses = new LinkedHashSet<InetSocketAddress>(
                    allLocalAddresses.size());
            for (SocketAddress socketAddress : allLocalAddresses)
            {
                addresses.add((InetSocketAddress) socketAddress);
            }
            return addresses;
        }
        catch (Throwable ignored)
        {
            return Collections.emptySet();
        }
    }

    @Override
    protected SocketAddress remoteAddress0()
    {
        try
        {
            Iterator<SocketAddress> i = ch.getRemoteAddresses().iterator();
            if (i.hasNext())
            {
                return i.next();
            }
        }
        catch (IOException e)
        {
            // ignore
        }
        return null;
    }

    @Override
    public Set<InetSocketAddress> allRemoteAddresses()
    {
        try
        {
            final Set<SocketAddress> allLocalAddresses = ch
                    .getRemoteAddresses();
            final Set<InetSocketAddress> addresses = new LinkedHashSet<InetSocketAddress>(
                    allLocalAddresses.size());
            for (SocketAddress socketAddress : allLocalAddresses)
            {
                addresses.add((InetSocketAddress) socketAddress);
            }
            return addresses;
        }
        catch (Throwable ignored)
        {
            return Collections.emptySet();
        }
    }

    @Override
    protected void doBind(SocketAddress localAddress) throws Exception
    {
        ch.bind(localAddress);
    }

    @Override
    protected void doConnect(SocketAddress remoteAddress,
            SocketAddress localAddress) throws Exception
    {
        if (localAddress != null)
        {
            ch.bind(localAddress);
        }

        boolean success = false;
        try
        {
            ch.connect(remoteAddress);
            boolean finishConnect = false;
            while (!finishConnect)
            {
                if (connectSelector.select(SO_TIMEOUT) >= 0)
                {
                    final Set<SelectionKey> selectionKeys = connectSelector
                            .selectedKeys();
                    for (SelectionKey key : selectionKeys)
                    {
                        if (key.isConnectable())
                        {
                            selectionKeys.clear();
                            finishConnect = true;
                            break;
                        }
                    }
                    selectionKeys.clear();
                }
            }
            success = ch.finishConnect();
        }
        finally
        {
            if (!success)
            {
                doClose();
            }
        }
    }

    @Override
    protected void doDisconnect() throws Exception
    {
        doClose();
    }

    @Override
    protected void doClose() throws Exception
    {
        closeSelector("read", readSelector);
        closeSelector("write", writeSelector);
        closeSelector("connect", connectSelector);
        ch.close();
    }

    private static void closeSelector(String selectorName, Selector selector)
    {
        try
        {
            selector.close();
        }
        catch (IOException e)
        {
            logger.warn("Failed to close a " + selectorName + " selector.", e);
        }
    }

    @Override
    public ChannelFuture bindAddress(InetAddress localAddress)
    {
        return bindAddress(localAddress, newPromise());
    }

    @Override
    public ChannelFuture bindAddress(final InetAddress localAddress,
            final ChannelPromise promise)
    {
        if (eventLoop().inEventLoop())
        {
            try
            {
                ch.bindAddress(localAddress);
                promise.setSuccess();
            }
            catch (Throwable t)
            {
                promise.setFailure(t);
            }
        }
        else
        {
            eventLoop().execute(new Runnable()
            {
                @Override
                public void run()
                {
                    bindAddress(localAddress, promise);
                }
            });
        }
        return promise;
    }

    @Override
    public ChannelFuture unbindAddress(InetAddress localAddress)
    {
        return unbindAddress(localAddress, newPromise());
    }

    @Override
    public ChannelFuture unbindAddress(final InetAddress localAddress,
            final ChannelPromise promise)
    {
        if (eventLoop().inEventLoop())
        {
            try
            {
                ch.unbindAddress(localAddress);
                promise.setSuccess();
            }
            catch (Throwable t)
            {
                promise.setFailure(t);
            }
        }
        else
        {
            eventLoop().execute(new Runnable()
            {
                @Override
                public void run()
                {
                    unbindAddress(localAddress, promise);
                }
            });
        }
        return promise;
    }

    private final class OioSctpChannelConfig extends DefaultSctpChannelConfig
    {
        private OioSctpChannelConfig(OioSctpChannel channel,
                SctpChannel javaChannel)
        {
            super(channel, javaChannel);
        }

        @Override
        protected void autoReadCleared()
        {
            clearReadPending();
        }
    }
}
